use std::io::{self, Write};
use std::process::Command;

// Needs the help of the futures_util crate for the catch_unwind function to use on our async test functions
use futures_util::future::FutureExt;

use rust_web_dev::{config, handle_errors, oneshot, setup_store};
use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Serialize, Deserialize, Debug, Clone)]
struct User {
    email: String,
    password: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
struct Question {
title: String,
content: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
struct QuestionAnswer {
id: i32,
title: String,
content: String,
tags: Option<Vec<String>>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
struct Token(String);

#[tokio::main]
async fn main() -> Result<(), handle_errors::Error> {
    println!("Init integration tests");
    dotenv::dotenv().ok();
    let config = config::Config::new().expect("Config can't be set");

    let s = Command::new("sqlx")
        .arg("database")
        .arg("drop")
        .arg("--database-url")
        .arg(format!(
            "postgres://{}:{}@{}:{}/{}",
            config.database_user,
            config.database_password,
            config.database_host,
            config.database_port,
            config.database_name
        ))
        .arg("-y")
        // The output function will create the final command, which we can use to execute later.
        .output()
        .expect("sqlx command failed to start");

    // Execute DB commands.
    // Uses the stdout function to write our command to the command line and execute it.
    // Execute the command via the write_all command, and
    // print out errors if they happen via the stderr field.
    io::stdout().write_all(&s.stderr).unwrap();

    let s = Command::new("sqlx")
        .arg("database")
        .arg("create")
        .arg("--database-url")
        .arg(format!(
            "postgres://{}:{}@{}:{}/{}",
            config.database_user,
            config.database_password,
            config.database_host,
            config.database_port,
            config.database_name
        ))
        .output()
        .expect("sqlx command failed to start");
    io::stdout().write_all(&s.stderr).unwrap();

    // Set up a new store instance with a db connection pool.
    let store = setup_store(&config).await?;

    // Start the web server via the oneshot function
    // and listen for a sender signal to shut it down.
    // We use an oneshot channel for communicating with a different part of the system
    // (the mock server) and triggering functions (like a shutdown) through messages.
    let handler = oneshot(store).await;

    let u = User {
        email: "test@email.com".to_string(),
        password: "password".to_string(),
    };

    let token;

    print!("Running register_new_user...");
    // AssertUnwindSafe (https://doc.rust-lang.org/beta/std/panic/struct.AssertUnwindSafe.html).
    // The AssertUnwindSafe wrapper from the Rust standard library wraps our function and variables.
    // AssertUnwindSafe signals that the variables we use are safe to unwind.
    // Unwinding means to remove the functions and variables from the stack in reverse order, to leave a clean slate in case of an exception.
    // catch_unwind: https://doc.rust-lang.org/beta/std/panic/fn.catch_unwind.html
    // Call catch_unwind (https://docs.rs/futures-util/latest/futures_util/future/trait.FutureExt.html#method.catch_unwind) from the futures_util crate, which “catches unwinding panics while polling the future.”
    let result = std::panic::AssertUnwindSafe(register_new_user(&u)).catch_unwind().await;
    match result {
        Ok(_) => println!("✓"),
        // If the function panics, we catch it via catch_unwind earlier, and stop the process here.
        Err(_) => {
            let _ = handler.sender.send(1);
            std::process::exit(1);
        }
    }

    print!("Running login...");
    match std::panic::AssertUnwindSafe(login(u)).catch_unwind().await {
        Ok(t) => {
            token = t;
            println!("✓");
        },
        Err(_) => {
            let _ = handler.sender.send(1);
            std::process::exit(1);
        }
    }

    print!("Running post_question...");
    match std::panic::AssertUnwindSafe(post_question(token)).catch_unwind().await {
        Ok(_) => println!("✓"),
        Err(_) => {
            let _ = handler.sender.send(1);
            std::process::exit(1);
        }
    }

    // Send any integer to shut down the server.
    let _ = handler.sender.send(1);
    Ok(())
}

async fn register_new_user(user: &User) {
    let client = reqwest::Client::new();
    let res = client
        .post("http://localhost:3030/registration")
        .json(&user)
        .send()
        .await
        .unwrap()
        .json::<Value>()
        .await
        .unwrap();
    assert_eq!(res, "Account added".to_string());
}


async fn login(user: User) -> Token {
    let client = reqwest::Client::new();
    let res = client
        .post("http://localhost:3030/login")
        .json(&user)
        .send()
        .await
        .unwrap();
    assert_eq!(res.status(), 200);
    res
        .json::<Token>()
        .await
        .unwrap()
}

async fn post_question(token: Token) {
    let q = Question {
        title: "First Question".to_string(),
        content: "How can I test?".to_string(),
    };
    let client = reqwest::Client::new();
    let res = client
        .post("http://localhost:3030/questions")
        .header("Authorization", token.0)
        .json(&q)
        .send()
        .await
        .unwrap()
        .json::<QuestionAnswer>()
        .await
        .unwrap();
    assert_eq!(res.id, 1);
    assert_eq!(res.title, q.title);
}
